今天會來說明 TypeScript 中內建 ReturnType
和 Parameters
的原始碼(像是上圖這樣),如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!
一樣讓我們先來簡單了解 ReturnType 和 Parameters 這兩個 Utility Types 的使用。
ReturnType<T>
是 TypeScript 內建的 Utility Type,它能夠接受一個參數 T
,這個參數滿足「函式型別」的話(即,T 要是函式型別的子集合),則會回傳這個函式「回傳值的型別(return type)」;否則,就會回傳 any
。
來看一下官網提供的幾個範例:
type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => number[]>; // number[]
ReturnType<T>
的 <>
中帶入的是 () => string
這個函式型別,因為這個函式會回傳 string
,所以 T1
會是 string
。(s: string) => number[]
會回傳 number[]
,所以 T1
會是 number[]
。如果帶入的型別不符合,則會回傳 any
:
type T3 = ReturnType<string>; // any
有個稍微特別的地方是,雖然 T3
的型別會是 any
,但 TypeScript 會在 string
的地方跳出錯誤提示,至於為什麼會這樣,等等看原始碼的時候就會瞭解了!
Parameters<T>
則是 TypeScript 內建的另一個 Utility Type,它能夠接受一個參數 T
,這個參數滿足「函式型別」的話(即,T 要是函式型別的子集合),則會以「 tuple type 來回傳函式的「參數(parameters)」,否則會回傳 never
。
一樣來看看幾個例子,這四個範例帶進去 T
的型別都能滿足函式型別:
type T1 = Parameters<(a: number, b: string) => number>; // [a: number, b: string]
type T2 = Parameters<(a: number[]) => number>; // [a: number[]]
type T3 = Parameters<(a: { firstName: string; lastName: string }) => string>; // [a: { firstName: string; lastName: string; }]
type T4 = Parameters<(...a: number[]) => number>; // number[]
<T>
內的 (a: number, b: string) => number
是函式型別的子集合,所以 Parameters
這個 Utility Type 就會把函式的參數用 tuple type 的方式回傳出來,因此 T1
會是 [a: number, b: string]
。T3
會是 number[]
而不是 [number[]]
了。如果帶入 <T>
的型別不是函式型別的子集合的話,則會得到 never
:
type T5 = Parameters<string>; // never
同樣的,雖然有得到 T5
的型別是 never
,但 TypeScript 會在 string
的地方跳出錯誤提示,一樣等等看原始碼時就會知道為什麼:
如果直接看 ReturnType
這個 Utility Type 的原始碼時,讀者會看到一個先前沒提到過的關鍵字 — infer
:
勢必要先了解 infer
才能理解 ReturnType
的原始碼,所以就先來看看這個 infer
怎麼用吧!
在前幾天講 Conditional Types 時筆者曾經提到 X extends Y ? T : F
中的 X extends Y
指的是「當 X 是 Y 的子集合」。但如果現在的 Y 並不是一個確切的型別,我們想要讓 TypeScript 幫我們推導其型別的話,就可以用 infer
這個關鍵字。
寫起來會像這樣:
infer R
中的這個 R
就是 TypeScript 自己推導出來的型別,而且它是可以當 Conditional Type 的條件為 true
時,這個 R
是可以直接被拿來當成回傳值使用的。
因為 infer
的概念比較抽象,透過實際範例會比較好理解,來我們先來看幾個例子。
前幾天在說明 Conditional Types 時,曾使用 Flatten
來做示範:
現在我們把 any[]
的部分修改成 (infer R)[]
,也就是 any
變成 infer R
(加上括號是為了讓 TS 在解析語法時不會混淆):
這時候 TS 就會根據使用者帶入的型別,自動推導這個 R
應該是什麼型別。同時,被推導出來的 R
還可以在條件為 true
時作為回傳值使用。
讀者可以猜想下面的 R
會是什麼呢?
type Flatten<T> = T extends (infer R)[] ? R : T;
type T1 = Flatten<number[]>; // number,且 R 會是 number
type T2 = Flatten<(string | number)[]>; // string | number,且 R 會是 string | number
type T3 = Flatten<number>; // number
如果 T
是陣列型別的子集合的話,則會回傳這個被推導出來的 R
,否則直接回傳 T
。
<>
的是 number[]
,所以 R 會被推論是 number
,因此 T1
就會是 number
。<>
的是 (string | number)[]
,所以 R 會被推論是 string | number
,因此 T2
就會是 string | number
。<>
的是 number
,而 number
並不是陣列型別的子集合,所以會直接回傳原本帶入 <>
的型別。讓我們再看另一個例子:
type InferResp<T> = T extends { response: infer R; status: number } ? R : T;
在這個 Utility Type 中,T
如果是 { response: ...; status: number }
的子集合,則會回傳 R
,否則會回傳 T
,但這裡並不清楚 response 的型別是什麼,所以使用 { response: infer R, ... }
來讓 TS 推論:
type T1 = InferResp<{ response: { data: 'foobar' }; status: 200 }>; // { data: 'foobar' }
type T2 = InferResp<{ status: 400 }>; // { status: 400 }
在上面的例子中可以看到,當帶入的 T
滿足 { response: ...; status: number }
時,R
就可以自動被推導成 {data: 'foobar'}
。但如果不滿足的話,就會直接回傳原本帶入的型別 { status: 400 }
。
從上面的例子中可以看到,infer
適合用在需要做條件判斷,但型別又不完全明確時使用。
這個 infer
很特別,需要多感覺一下,等等說明 ReturnType
的原始碼時可以再體會看看。有幾個使用 infer
時一定要留意的細節:
infer
只能在 Conditional Types 中的 extends
被使用(更確切來說是 extends
後且 ?
前),不能在限制泛型(Generics Constraint)中的 extends
使用。infer R
後,這個被推導出來的型別 R
雖然能夠被當成型別直接回傳,但它只能用在符合 True 的條件使用(即,?
後且 :
前),不能用在 False 的情況(即,:
後)在認識 infer
之後,讓我們回頭來看 ReturnType
和 Parameters
這兩個 Utility Types 的原始碼。
要看懂這兩個 Utility Types 的原始碼,前面幾天提到的知識缺一不可:
infer
keywordReturnType
的原始碼是:
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
在看他人寫的 Utility Types 時,很重要的是做正確的斷句,斷好句後通常就會比較好理解它的意思,這裡我們來幫它斷句一下:
先看泛型的部分,也就是 <T extends (...args: any) => any>
,這裡面同時有兩個 >
在內,一開始會讓人容易有點混淆,但讀者只要知道 (...args: any) => any
這個是 TypeScript 的 Function Type,意思就是這個函式可以接受任何型別作為參數,也可回傳任何型別的值。
根據前幾天對於 Generics Constraint 的說明,將可以理解 <T extends (...args: any) => any>
完整的意思就是說,T
需要是 (...args: any) => any
的子集合,也就是說 T
需要滿足函式型別,不論這個函式的參數和回傳值的型別是什麼都可以。
現在讀者應該可以知道,為什麼在剛剛的範例中使用
ReturnType<string>
時,TS 會回報錯誤提示了,這是因為string
並不是函式型別的子集合。
接著把注意力放到等號後的 Conditional Types,T extends (...args: any) => infer R ? R : any;
,根據前幾天對 Conditional Types 的說明,讀者應該可以知道,這裡的判斷式 T extends (...args: any) => infer R
,如果這個判斷式為真,就會回傳 R
,否則會回傳 any
。
看到這裡讀者應該可以理解,為什麼在剛剛的範例中使用 ReturnType<string>
時,雖然 TS 有報錯,但最終還是可以得到 any
的型別。因為如果帶入的 T
不滿足函式型別的話,就會得到 any
:
最後來看看當使用者帶入的 T
滿足函式型別時,回傳的 R
是什麼。這個 R
就和今天認識的 infer
有關,在 Conditional Types 中的 T extends (...args: any) => infer R
,這裡用了 infer R
來推論這個函式會回傳的型別,並把這個函式會回傳的型別取名為 R
,所以 R
指的就是函式會回傳的型別。
最後來看看 Parameters<T>
,它的原始碼是:
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
要看懂這段一樣需要先試著將它們斷句,讀者們可以先自己試著練習看看。
斷句後我們可以看到,前面 Generic Constraints 的部分是 <T extends (...args: any) => any>
,需要滿足的限制是 (...args: any) => any
,這個部分和 ReturnType
是一樣的,也就是只要符合函式型別即可,不論該函式的參數或回傳值的型別是什麼。
在等號後的 Conditional Types 中可以看到如果 T
是函式型別的子集合(即,T extends (...args: infer P) => any
),就會回傳 P
,否則會回傳 never
。
那麼這個 P 是什麼呢?從 (...args: infer P)
可以看出這個 P
是透過 infer
來推論帶入 <>
中函式的「參數的型別」,推論後命名為 P
,並作為回傳值使用。
https://tsplay.dev/WYY9rW @ TypeScript Playground
infer
只能用在 Conditional Types 的 extends
後與 ?
前,不能用在 Generics Constraint 的 extends
後infer
推導出來的型別只能在 Conditional Types 中為 true 時被使用,不能在 false 是被使用infer
出來的型別是什麼,就回傳出來看看